/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved.
 */

import { CoreTask, DurationEvent, iconv, MetricLogType, SubmitOption, TaskState } from '@ohos/hvigor';
import { WorkerPoolDelegator } from '@ohos/hvigor';
import { isWindows } from '@ohos/hvigor';
import { spawn, SpawnOptionsWithoutStdio, spawnSync, SpawnSyncOptions, SpawnSyncReturns } from 'child_process';
import fs from 'fs';
import * as os from 'os';
import path from 'path';

import { BooleanCallback } from '../common/type/callback-type.js';
import { ECM } from '../error/error-code-map.js';
import { TaskNames } from '../tasks/common/task-names.js';

import { LogCombineType } from './log/log-combine-type';
import { OhosLogger } from './log/ohos-logger.js';

/**
 * 执行命令的工具类
 *
 * @since 2021-12-14
 */
export class ProcessUtils {
  private _log: OhosLogger;
  private readonly _moduleName: string;
  private readonly _taskName: string;
  private readonly _errLog: string;
  private readonly _ohosCharset: string;
  private readonly _solution: string;
  private readonly _defaultOptions: SpawnSyncOptions = {
    encoding: null,
    windowsHide: true,
  };

  constructor(
    moduleName = 'root',
    taskName = 'defaultTask',
    logLevel = '',
    errLog = 'Tools execution failed.',
    solution = 'Please check the message from tools.',
    charset = isWindows() ? 'GBK' : 'utf-8',
  ) {
    this._moduleName = moduleName;
    this._taskName = taskName;
    this._errLog = errLog;
    this._solution = solution;
    this._ohosCharset = charset;
    this._log = OhosLogger.getLogger(`${moduleName}:${taskName}`);
    if (logLevel) {
      this._log.setLevel(logLevel);
    }
  }

  /**
   * 向工作池提交同步执行的命令，执行命令无返回值
   *
   * @param task
   * @param workerPool 工作池代理器
   * @param inputData 输入数据
   * @param callback 回调函数
   * @param callbackInput 回调函数的输入数据
   * @param subDurationEvent 子持续事件
   * @param targetWorkers 目标worker的集合
   * @param combine
   */
  async submitExecutionWithoutReturn(
    task: CoreTask,
    workerPool: WorkerPoolDelegator,
    inputData: any,
    callback: Function,
    callbackInput: unknown[],
    subDurationEvent: DurationEvent,
    targetWorkers?: number[],
    combine = LogCombineType.WARN,
  ): Promise<void> {
    inputData = { ...inputData };
    const submitOption: SubmitOption = {
      workInput: inputData,
      callback: callback,
      callbackInput: callbackInput,
      targetWorkers: targetWorkers,
    };
    const workPath: string = path.resolve(__dirname, '../tasks/worker/worker-process-script.js/generalExecute');
    if (workerPool.submit(task, workPath, submitOption).getState() === TaskState.REJECT) {
      const executeTaskName = 'execute the command';
      const subToolDurationEvent = subDurationEvent.createSubEvent(executeTaskName, '');
      subToolDurationEvent.start();
      await this.execute(inputData.commandLine, inputData.commandOptions, combine);
      if (inputData.integratedHspCommand) {
        await this.execute(inputData.integratedHspCommand);
      }
      await callback(...callbackInput);
      subToolDurationEvent.stop();
      subToolDurationEvent.setLog(executeTaskName, MetricLogType.INFO);
    }
  }

  /**
   * 向工作池提交同步执行的命令，执行命令有返回值，作为callback的输入
   *
   * @param task
   * @param workerPool 工作池代理器
   * @param funcPath 执行函数的路径
   * @param inputData 输入数据
   * @param callback 回调函数
   * @param targetWorkers 目标worker的集合
   * @returns {boolean} 是否成功提交
   */
  submitSyncExecutionWithReturn(
    task: CoreTask,
    workerPool: WorkerPoolDelegator,
    funcPath: string,
    inputData: any,
    callback: Function,
    targetWorkers?: number[],
  ): boolean {
    inputData = { ...inputData };
    const submitOption: SubmitOption = {
      workInput: inputData,
      callback: callback,
      targetWorkers: targetWorkers,
      useReturnVal: true,
    };
    return workerPool.submit(task, path.resolve(__dirname, '../tasks/worker/worker-process-script.js', funcPath), submitOption).getState() !== TaskState.REJECT;
  }

  async execute(command: string[], options?: SpawnSyncOptions, combine = LogCombineType.WARN, specialLogCallBack?: BooleanCallback) {
    const mergedOptions: SpawnSyncOptions = this.processOptionsFactory(options);
    const result = {
      stdout: '',
      stderr: '',
    };
    try {
      this.validateExecuteFile(command[0]);
      this._log.debug('current process  memoryUsage:', process.memoryUsage(), `os memoryUsage :${(os.totalmem() - os.freemem()) / 1024 / 1024 / 1024}`);
      const spawnChildProcess = spawn(command[0], command.slice(1), mergedOptions);
      spawnChildProcess.stdout?.on('data', (data) => {
        const stdoutStr = iconv.decode(<Buffer>data, this._ohosCharset);
        this._log.debug(stdoutStr);

        if (this.needPrintWarnWhenNoReturnValue()) {
          this.nativeNoReturnValueWarningHandler(stdoutStr, combine);
        }
        result.stdout += stdoutStr;
      });

      spawnChildProcess.stderr?.on('data', (data) => {
        const stderrStr = iconv.decode(<Buffer>data, this._ohosCharset);
        result.stderr += stderrStr;
      });

      // 特殊日志处理回调，处理完毕后则跳过后续通用日志输出
      await new Promise<void>((resolve, reject) => {
        spawnChildProcess.once('close', (code) => {
          if (code === 0) {
            if (specialLogCallBack && specialLogCallBack(result, this._ohosCharset)) {
              resolve();
              return;
            }
            if (combine === LogCombineType.DEBUG) {
              this._log.debug(result.stderr);
            }
            if (combine === LogCombineType.WARN) {
              this._log.warn(result.stderr);
            }
            resolve();
          } else {
            reject(this.makeError(result.stdout.trim(), result.stderr.trim(), '', code, command.join(' '), command.join(' ')));
          }
        });
      });
    } catch (e) {
      this.handleException(e, combine);

      // 代码到不了这里, 为了规避es-lint
      throw e;
    }
    return result;
  }

  /**
   * 同步地执行命令，并打印结果。同步的执行方式必然需要等到执行结束
   *
   * @param command 命令
   * @param options - 传入参数
   * @param combine
   */
  executeSync(command: string[], options?: SpawnSyncOptions, combine = LogCombineType.WARN): SpawnSyncReturns<string | Buffer> {
    const mergedOptions: SpawnSyncOptions = this.processOptionsFactory(options);
    let result: SpawnSyncReturns<string | Buffer>;
    try {
      this.validateExecuteFile(command[0]);
      result = spawnSync(command[0], command.slice(1), mergedOptions);
      const stdout = iconv.decode(<Buffer>result.stdout, this._ohosCharset);
      const stderr = iconv.decode(<Buffer>result.stderr, this._ohosCharset);
      if (result.error || result.status !== 0) {
        const stdError = this.makeError(stdout.trim(), stderr.trim(), result.error, result.status, command.join(' '), command.join(' '));
        this.handleException(stdError, combine);
      }
      if (result.stdout?.length > 0) {
        this._log.debug(stdout);
      }
      if (result.stderr?.length > 0) {
        if (combine === LogCombineType.DEBUG) {
          this._log.debug(stderr);
        }
        if (combine === LogCombineType.WARN) {
          this._log.warn(stderr);
        }
      }
    } catch (e: any) {
      const error = this.makeError('', '', e, e.status, command.join(' '), command.join(' '));
      this.handleException(error, combine);
    }
    return result!;
  }

  private handleOutput(result: SpawnSyncReturns<string | Buffer>, combine: LogCombineType) {
    if (result.stderr?.length > 0) {
      const stderrStr = iconv.decode(<Buffer>result.stderr, this._ohosCharset);
      if (combine === LogCombineType.DEBUG) {
        this._log.debug(stderrStr);
      }
      if (combine === LogCombineType.WARN) {
        this._log.warn(stderrStr);
      }
    }
    if (result.status !== 0) {
      // 打印报错,但不退出
      this._log
        ._buildError(this._errLog)
        ._solution(iconv.decode(<Buffer>result.stderr, this._ohosCharset))
        ._printError(this._moduleName);
    }
  }

  private handleException(e: any, combine: LogCombineType) {
    if (e.stderr && e.stderr.length > 0) {
      // 打印命令内容，便于定界定位问题，处理下密码等敏感信息
      const regex = /(-keystorePwd|-keyPwd) \S+/g;
      const command = e.command?.replace(regex, '$1 ***');
      this._log.debug(`ERROR: command = ${command}`);
      this._log._buildError(combine === LogCombineType.IGNORE ? `${this._errLog}${os.EOL}${e.message}` : `${this._errLog}${os.EOL}${e.stderr}`);
      this._log._solution(this._solution);
    } else {
      this._log._buildError(`${this._errLog}${os.EOL}${e.message}`);
    }
    this._log._errorCode(ECM.DECE.PROCESS_UTILS_HANDLE_EXCEPTION);
    this._log._printErrorAndExit(this._moduleName);
  }

  /**
   * 同步地执行命令，并打印结果。
   *
   * @param {string} command 命令
   * @param {SpawnSyncOptions} options - 传入参数
   * @return {SpawnSyncReturns<string | Buffer>} 返回信息
   */
  execaCommandSync(command: string, options?: SpawnSyncOptions): SpawnSyncReturns<string | Buffer> {
    const result = spawnSync(command, options);
    this.printExecaResultLog(result);
    return result!;
  }

  /**
   * 异步执行命令，并打印结果。
   *
   * @param {string} command 命令
   * @param {SpawnOptionsWithoutStdio} options - 传入参数
   * @return {Promise<void>} 返回信息
   */
  async execaCommand(command: string, options?: SpawnOptionsWithoutStdio): Promise<void> {
    const mergedOptions = this.processOptionsFactory(options);
    await new Promise((resolve, reject) => {
      const result = spawn(command, mergedOptions);
      let stdoutMsg = '';
      let stderrMsg = '';
      result.stdout?.on('data', (data) => {
        stdoutMsg += iconv.decode(<Buffer>data, this._ohosCharset);
      });

      result.stderr?.on('data', (data) => {
        stderrMsg += iconv.decode(<Buffer>data, this._ohosCharset);
      });

      result.on('close', (code) => {
        if (code !== 0) {
          const errorMsg = stdoutMsg + stderrMsg;
          reject(errorMsg);
        } else {
          resolve(stdoutMsg);
        }
      });
    })
      .then((stdoutMsg) => {
        this._log.info(stdoutMsg);
      })
      .catch((stderrMsg) => {
        this._log._buildError(this._errLog)._solution(stderrMsg)._printErrorAndExit();
      });
  }

  /**
   * 打印execa执行命令后的返回结果
   *
   * @param {ExecaReturnValue, ExecaSyncReturnValue}result 日志结果对象
   */
  printExecaResultLog(
    result:
      | SpawnSyncReturns<string | Buffer>
      | {
          stdout: string | Buffer;
          stderr: string | Buffer;
          status?: number;
        },
  ): void {
    if (result.stdout?.length > 0) {
      this._log.info(iconv.decode(<Buffer>result.stdout, this._ohosCharset));
    }
    if (result.stderr?.length > 0) {
      this._log.warn(iconv.decode(<Buffer>result.stderr, this._ohosCharset));
    }
    if (result.status !== 0) {
      // 打印报错,但不退出
      this._log
        ._buildError(this._errLog)
        ._solution(iconv.decode(<Buffer>result.stderr, this._ohosCharset))
        ._printErrorAndExit();
    }
  }

  /**
   * 根据process状态判断该报错是否需要打印到输出流中
   *
   * @returns {boolean}
   * @private
   */
  private needPrintWarnWhenNoReturnValue(): boolean {
    return this._taskName.endsWith(TaskNames.Task.BUILD_NATIVE_WITH_NINJA.name) || this._taskName.endsWith(TaskNames.Task.BUILD_NATIVE_WITH_CMAKE.name);
  }

  /**
   * 生成execa需要的options入参
   *
   * @param options - 传入参数
   */
  processOptionsFactory(options: SpawnSyncOptions | undefined | SpawnOptionsWithoutStdio): SpawnSyncOptions {
    const originalPath: string = process.env.PATH ?? '';
    if (options) {
      options.windowsHide = true;
    }
    if (options?.env) {
      options.env = {
        ...process.env,
        ...options.env,
      };
    }
    if (options?.env?.path) {
      options.env.PATH = originalPath + (isWindows() ? ';' : ':') + options.env.path;
    }

    return {
      ...this._defaultOptions,
      ...options,
    };
  }

  private validateExecuteFile(file: string) {
    if (file === 'java' || file === 'javac') {
      return;
    }
    if (!fs.existsSync(path.normalize(file))) {
      this._log.printErrorExit('FILE_NOT_FOUNF_OR_UNEXCUTABLE', [file], [[file], [file]]);
    }
  }

  private makeError(stdout: string, stderr: string, error: any, exitCode: number | null, command: string, escapedCommand: string) {
    const spawnMessage = exitCode !== undefined ? `Command failed with exit code ${exitCode}: ${command}` : `failed: ${command}`;

    const isError = Object.prototype.toString.call(error) === '[object Error]';
    const shortMessage = isError ? `${spawnMessage}\n${error.message}` : spawnMessage;
    const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n');

    if (isError) {
      error.originalMessage = error.message;
      error.message = message;
    } else {
      error = new Error(message);
    }

    error.shortMessage = shortMessage;
    error.command = command;
    error.escapedCommand = escapedCommand;
    error.exitCode = exitCode;
    error.stdout = stdout;
    error.stderr = stderr;

    if ('bufferedData' in error) {
      delete error.bufferedData;
    }

    error.failed = true;
    return error;
  }

  /**
   * native编译特殊日志处理
   *
   * @param {string} studio 执行命令返回结果
   * @param {LogCombineType} combine 日志级别
   * @private
   */
  private nativeNoReturnValueWarningHandler(studio: string, combine: LogCombineType) {
    const regExpCase1 = /warning: non-void function does not return a value/;
    if (regExpCase1.test(studio) && combine === LogCombineType.WARN) {
      this._log.warn(studio);
    }
  }
}
